package com.gjy.design.strategy.compression;

import org.apache.commons.compress.archivers.zip.ZipArchiveEntry;
import org.apache.commons.compress.archivers.zip.ZipArchiveOutputStream;
import org.apache.commons.compress.archivers.zip.ZipFile;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;

import java.io.*;
import java.nio.file.Files;
import java.util.Enumeration;

/**
 * ZIP压缩策略的具体实现。
 * ZIP是一种归档格式，能够包含多个文件和目录结构。
 *
 * @author gjy
 * @version 1.0
 * @since 2025-11-06 14:31:21
 */
public class ZipCompressionStrategy implements CompressionStrategy {
    private static final Logger log = LoggerFactory.getLogger(ZipCompressionStrategy.class);

    @Override
    public void compress(File[] source, File target) throws IOException {
        // 输入校验
        if (source == null || source.length == 0) {
            throw new IllegalArgumentException("源文件/文件夹数组不能为空");
        }
        if (target.isDirectory()) {
            throw new IllegalArgumentException("目标必须是一个文件，而不是目录");
        }
        if (target.exists() && !target.canWrite()) {
            throw new IOException("无法写入目标文件: " + target.getAbsolutePath());
        }

        // 确保目标文件的父目录存在
        File parent = target.getParentFile();
        if (parent != null && !parent.exists()) {
            boolean b = parent.mkdirs();
            log.warn("compress 创建目录: {}", b);
        }

        // 使用 try-with-resources 确保流被正确关闭
        try (OutputStream fos = Files.newOutputStream(target.toPath());
             ZipArchiveOutputStream zaos = new ZipArchiveOutputStream(fos)) {

            // 遍历所有源文件/文件夹
            for (File file : source) {
                if (file == null || !file.exists()) {
                    System.err.println("警告: 源文件/文件夹不存在，将被跳过: " + (file == null ? "null" : file.getAbsolutePath()));
                    continue;
                }

                if (file.isDirectory()) {
                    // 如果是目录，递归地添加到ZIP中
                    addToZipDirectory(file, file.getName(), zaos);
                } else {
                    // 如果是文件，直接添加到ZIP中
                    addToZipFile(file, zaos);
                }
            }
            // 完成ZIP文件的写入
            zaos.finish();
        }
    }

    @Override
    public void decompress(File source, File target) throws IOException {
        // 输入校验
        if (!source.isFile()) {
            throw new IllegalArgumentException("源文件必须是文件，而不是目录");
        }
        if (!target.exists()) {
            // 如果目标目录不存在，则创建
            boolean b = target.mkdirs();
            log.warn("decompress 创建目录: {}", b);
        }
        if (!target.isDirectory()) {
            throw new IllegalArgumentException("目标必须是目录，而不是文件");
        }

        // 使用 try-with-resources
        try (ZipFile zipFile = ZipFile.builder().setFile(source).get()) {
            // 遍历ZIP文件中的每个条目
            Enumeration<ZipArchiveEntry> entries = zipFile.getEntries();
            while (entries.hasMoreElements()) {
                ZipArchiveEntry entry = entries.nextElement();
                File entryFile = new File(target, entry.getName());
                // 防止ZIP Slip漏洞，确保文件在目标目录内
                if (!entryFile.toPath().normalize().startsWith(target.toPath().normalize())) {
                    throw new IOException("解压文件时存在路径遍历风险: " + entry.getName());
                }

                if (entry.isDirectory()) {
                    // 如果是目录条目，创建对应的物理目录
                    boolean b = entryFile.mkdirs();
                    log.warn("解压目录创建: {}", b);
                } else {
                    // 如果是文件条目，确保其父目录存在并写入文件内容
                    File parent = entryFile.getParentFile();
                    if (parent != null && !parent.exists()) {
                        boolean b = parent.mkdirs();
                        log.warn("解压文件创建目录: {}", b);
                    }
                    try (InputStream in = zipFile.getInputStream(entry);
                         OutputStream out = Files.newOutputStream(entryFile.toPath())) {
                        byte[] buffer = new byte[8192]; // 使用更大的缓冲区
                        int len;
                        while ((len = in.read(buffer)) > 0) {
                            out.write(buffer, 0, len);
                        }
                    }
                }
            }
        }
    }

    /**
     * 递归地将一个目录及其所有内容添加到ZIP输出流中。
     *
     * @param dir  要添加的目录。
     * @param path 该目录在ZIP文件中的路径前缀。
     * @param zaos ZIP输出流。
     * @throws IOException 如果发生I/O错误。
     */
    private void addToZipDirectory(File dir, String path, ZipArchiveOutputStream zaos) throws IOException {
        File[] children = dir.listFiles();
        if (children != null) {
            for (File child : children) {
                String childPath = path + File.separator + child.getName();
                if (child.isDirectory()) {
                    addToZipDirectory(child, childPath, zaos);
                } else {
                    addToZipFile(child, childPath, zaos);
                }
            }
        }
    }

    /**
     * 将单个文件添加到ZIP输出流中。
     *
     * @param fileToCompress 要压缩的文件。
     * @param zaos           ZIP输出流。
     * @throws IOException 如果发生I/O错误。
     */
    private void addToZipFile(File fileToCompress, ZipArchiveOutputStream zaos) throws IOException {
        addToZipFile(fileToCompress, fileToCompress.getName(), zaos);
    }

    /**
     * 将单个文件以指定的路径添加到ZIP输出流中。
     *
     * @param fileToCompress 要压缩的文件。
     * @param entryName      该文件在ZIP条目中的名称/路径。
     * @param zaos           ZIP输出流。
     * @throws IOException 如果发生I/O错误。
     */
    private void addToZipFile(File fileToCompress, String entryName, ZipArchiveOutputStream zaos) throws IOException {
        ZipArchiveEntry entry = new ZipArchiveEntry(fileToCompress, entryName);
        zaos.putArchiveEntry(entry);
        try (FileInputStream fis = new FileInputStream(fileToCompress)) {
            byte[] buffer = new byte[8192];
            int len;
            while ((len = fis.read(buffer)) > 0) {
                zaos.write(buffer, 0, len);
            }
        }
        zaos.closeArchiveEntry();
    }
}
